﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Specialized;
using System.Windows.Threading;
using System.Threading;
using System.Diagnostics;

namespace MVVMFramework.ViewModel
{
    /// <summary>
    /// Contains a list of objects and current element of the specified type. 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class BrowseEntity<T> : NotifyPropertyChangedBase, IList<T>, IList, INotifyCollectionChanged where T : class
    {
        private readonly List<T> entities;

        /// <summary>
        /// Initializes a new instance of the <see cref="BrowseEntity&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="comparer">The comparer of the objects. Used to compare two objects using business logic, for example, by id or using group of properties.</param>
        public BrowseEntity(Func<T, T, bool> comparer = null)
            : base(Dispatcher.CurrentDispatcher)
        {
            syncRoot = new object();
            entities = new List<T>();
            this.comparer = comparer;

            firstEntryIsDefault = true;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BrowseEntity&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="entities">The collection whose elements are copied to the new collection.</param>
        /// <param name="comparer">The comparer of the objects. Used to compare two objects using business logic, for example, by id or using group of properties.</param>
        public BrowseEntity(IEnumerable<T> entities, Func<T, T, bool> comparer = null)
            : this(comparer)
        {
            if (entities != null)
            {
                this.entities.AddRange(entities);
                if (FirstEntryIsDefault && Count > 0)
                    Current = this.entities[0];
            }
        }

        /// <summary>
        /// Occurs when the collection changes.
        /// </summary>
        public event EventHandler Changed;
        /// <summary>
        /// Occurs when the current element in the collection changes.
        /// </summary>
        public event EventHandler CurrentChanged;

        private bool loading;
        public bool Loading
        {
            get
            {
                return loading;
            }
            set
            {
                if (loading != value)
                {
                    loading = value;
                    OnPropertyChanged(() => Loading);
                }
            }
        }

        private string loadingError;
        public string LoadingError
        {
            get
            {
                return loadingError;
            }
            set
            {
                if (loadingError != value)
                {
                    loadingError = value;
                    OnPropertyChanged(() => LoadingError);
                }
            }
        }

        private Func<T, T, bool> comparer;
        /// <summary>
        /// Gets or sets the comparer of two objects.
        /// </summary>
        /// <value>
        /// The object comparer.
        /// </value>
        public Func<T, T, bool> Comparer
        {
            get
            {
                return comparer;
            }
            set
            {
                comparer = value;
            }
        }

        #region Current

        private T current;
        /// <summary>
        /// Gets or sets the current element of the collection.
        /// </summary>
        /// <value>
        /// The current element.
        /// </value>
        public T Current
        {
            get
            {
                return current;
            }
            set
            {
                if (current != value)
                {
                    if (value != null && !entities.Contains(value))
                        throw new Exception("Attempt to set the current item that is not in the collection");

                    current = value;
                    RaiseCurrentChanged();
                }
            }
        }

        /// <summary>
        ///Sets the new current element using <see cref="Comparer"/> to find appropriate value.
        /// </summary>
        /// <param name="value">The value.</param>
        public void SetCurrentAs(T value)
        {
            if (Current != value || value == null)
            {
                value = findByComparer(value);
                if (FirstEntryIsDefault && value == null && entities.Count > 0)
                    value = entities[0];
                Current = value;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the current element is selected.
        /// </summary>
        public bool IsCurrentSelected
        {
            get
            {
                return Current != null;
            }
        }

        #endregion

        /// <summary>
        /// Refills the collection by specified entities.
        /// </summary>
        /// <param name="newEntities">The collection whose elements are copied to the collection.</param>
        /// <param name="restoreCurrent">if set to <c>true</c> [restore current element].</param>
        public void Refill(IEnumerable<T> newEntities, bool restoreCurrent = true)
        {
            if (newEntities == null)
            {
                entities.Clear();
            }
            else
            {
                T oldCurrent = null;

                if (restoreCurrent)
                    oldCurrent = Current;

                entities.Clear();
                entities.AddRange(newEntities);

                OnReset();

                if (restoreCurrent)
                    SetCurrentAs(oldCurrent);
            }
        }

        private bool firstEntryIsDefault;
        /// <summary>
        /// Gets or sets a value indicating whether the first entry is default. In case when the first element is default then the current element will be a first entity by default.
        /// </summary>
        public bool FirstEntryIsDefault
        {
            get
            {
                return firstEntryIsDefault;
            }
            set
            {
                if (firstEntryIsDefault != value)
                {
                    firstEntryIsDefault = value;
                    OnPropertyChanged(() => FirstEntryIsDefault);
                }
            }
        }

        #region IList<T>

        /// <summary>
        /// Inserts an entity to the <see cref="T:System.Collections.Generic.IList`1"></see> at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index at which entity should be inserted.</param>
        /// <param name="item">The object to insert into the <see cref="T:System.Collections.Generic.IList`1"></see>.</param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IList`1"></see> is read-only.</exception>
        ///   
        /// <exception cref="T:System.ArgumentOutOfRangeException">index is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"></see>.</exception>
        public void Insert(int index, T item)
        {
            entities.Insert(index, item);
            OnItemAdded(item);
        }

        /// <summary>
        /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
        /// </summary>
        /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</param>
        /// <returns>
        /// true if item was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"></see>; otherwise, false. This method also returns false if item is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"></see>.
        /// </returns>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only.</exception>
        public bool Remove(T item)
        {
            int index = entities.IndexOf(item);
            bool removed = entities.Remove(item);
            if (removed)
            {
                Current = null;
                OnItemRemoved(index, item);
                if (index >= entities.Count)
                    index--;

                Current = index >= 0 ? entities[index] : null;
            }
            return removed;
        }

        /// <summary>
        /// Removes the <see cref="T:System.Collections.Generic.IList`1"></see> entity at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index of the entity to remove.</param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IList`1"></see> is read-only.</exception>
        ///   
        /// <exception cref="T:System.ArgumentOutOfRangeException">index is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"></see>.</exception>
        public void RemoveAt(int index)
        {
            T oldEntry = entities[index];
            int removedIndex = index;
            Current = null;
            entities.RemoveAt(index);
            OnItemRemoved(index, oldEntry);
            if (removedIndex >= entities.Count)
                removedIndex--;
            Current = removedIndex >= 0 ? entities[removedIndex] : null;
        }

        /// <summary>
        /// Gets or sets the element at the specified index.
        /// </summary>
        /// <returns>The element at the specified index.</returns>
        ///   
        /// <exception cref="T:System.ArgumentOutOfRangeException">index is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"></see>.</exception>
        ///   
        /// <exception cref="T:System.NotSupportedException">The property is set and the <see cref="T:System.Collections.Generic.IList`1"></see> is read-only.</exception>
        public T this[int index]
        {
            get
            {
                return entities[index];
            }
            set
            {
                if (entities[index] != value)
                {
                    T oldValue = entities[index];
                    bool setCurrent = entities[index] == Current;
                    entities[index] = value;
                    OnItemReplaced(oldValue, value);
                    if (setCurrent && comparer != null && comparer(Current, value))
                        SetCurrentAs(value);
                    else
                        SetCurrentAs(null);
                }
            }
        }

        /// <summary>
        /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
        /// </summary>
        /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only.</exception>
        public void Add(T item)
        {
            Insert(this.Count, item);
        }

        /// <summary>
        /// Removes all entities from the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
        /// </summary>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only. </exception>
        public void Clear()
        {
            if (Count > 0)
            {
                entities.Clear();
                Current = null;
                OnReset();
            }
        }

        /// <summary>
        /// Determines the index of a specific entity in the <see cref="T:System.Collections.Generic.IList`1"></see>.
        /// </summary>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.IList`1"></see>.</param>
        /// <returns>
        /// The index of item if found in the list; otherwise, -1.
        /// </returns>
        public int IndexOf(T item)
        {
            return entities.IndexOf(item);
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"></see> contains a specific value.
        /// </summary>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</param>
        /// <returns>
        /// true if entity is found in the <see cref="T:System.Collections.Generic.ICollection`1"></see>; otherwise, false.
        /// </returns>
        public bool Contains(T item)
        {
            return entities.Contains(item);
        }

        /// <summary>
        /// Copies all the elements of the collection to the specified one-dimensional Array starting at the specified destination Array index. The index is specified as a 32-bit integer.
        /// </summary>
        /// <param name="array">The one-dimensional Array that is the destination of the elements copied from the current Array.</param>
        /// <param name="arrayIndex">A 32-bit integer that represents the index in array at which copying begins.</param>
        public void CopyTo(T[] array, int arrayIndex)
        {
            entities.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
        /// </summary>
        /// <returns>The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</returns>
        public int Count
        {
            get
            {
                return entities.Count;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only.
        /// </summary>
        /// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only; otherwise, false.</returns>
        public bool IsReadOnly
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator<T> GetEnumerator()
        {
            return entities.GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return entities.GetEnumerator();
        }

        #endregion

        /// <summary>
        /// Called when entity added to the collection.
        /// </summary>
        /// <param name="items">The added entities.</param>
        protected void OnItemAdded(params T[] items)
        {
            if (notifycationSuspended)
                collectionChanged = true;
            else
                RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items));
            RaiseChanged();
        }

        /// <summary>
        /// Called when item removed from the collection.
        /// </summary>
        /// <param name="index">The index of the removed entity.</param>
        /// <param name="items">The removed items.</param>
        protected void OnItemRemoved(int index, params T[] items)
        {
            if (notifycationSuspended)
                collectionChanged = true;
            else
                RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items, index));
            RaiseChanged();
        }

        /// <summary>
        /// Called when reset of the collection occurred.
        /// </summary>
        protected void OnReset()
        {
            if (notifycationSuspended)
                collectionChanged = true;
            else
                RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            RaiseChanged();
        }

        /// <summary>
        /// Called when entity is replaced.
        /// </summary>
        /// <param name="oldItem">The old item.</param>
        /// <param name="newItem">The new item.</param>
        protected void OnItemReplaced(T oldItem, T newItem)
        {
            RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem));

            RaiseChanged();
        }

        /// <summary>
        /// Raises the <see cref="Changed"/> event.
        /// </summary>
        protected void RaiseChanged()
        {
            EventHandler changed = Changed;
            if (changed != null)
                changed(this, EventArgs.Empty);
        }

        /// <summary>
        /// Raises the <see cref="CurrentChanged"/> event.
        /// </summary>
        protected void RaiseCurrentChanged()
        {
            OnPropertyChanged(() => Current);
            OnPropertyChanged(() => IsCurrentSelected);
            EventHandler currentChanged = CurrentChanged;
            if (currentChanged != null)
                currentChanged(this, EventArgs.Empty);
        }

        private T findByComparer(T item)
        {
            if (item == null)
                return null;
            if (entities.Contains(item))
                return item;
            if (comparer != null)
                return entities.FirstOrDefault(i => comparer(i, item));
            return null;
        }

        #region INotifyCollectionChanged

        bool notifycationSuspended;
        bool collectionChanged;

        /// <summary>
        /// Suspends the notifications about <see cref="Changed"/> event.
        /// </summary>
        public void SuspendNotification()
        {
            notifycationSuspended = true;
        }

        /// <summary>
        /// Resumes the notifications about <see cref="Changed"/> event.
        /// </summary>
        public void ResumeNotification()
        {
            if (notifycationSuspended)
            {
                if (collectionChanged)
                {
                    RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    collectionChanged = false;
                }
                notifycationSuspended = false;
            }
        }

        /// <summary>
        /// Raises the collection changed event.
        /// </summary>
        /// <param name="args">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            if (Dispatcher.Thread != Thread.CurrentThread)
            {
                Dispatcher.Invoke(new Action<NotifyCollectionChangedEventArgs>(RaiseCollectionChanged), args);
            }
            else
            {
                NotifyCollectionChangedEventHandler handler = CollectionChanged;
                if (handler != null)
                    handler(this, args);
            }
        }

        /// <summary>
        /// Occurs when the collection changes.
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion

        #region IList

        /// <summary>
        /// Adds an item to the <see cref="T:System.Collections.IList"/>.
        /// </summary>
        /// <param name="value">The object to add to the <see cref="T:System.Collections.IList"/>.</param>
        /// <returns>The position into which the new element was inserted, or -1 to indicate that the item was not inserted into the collection,</returns>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.IList"/> is read-only.-or- The <see cref="T:System.Collections.IList"/> has a fixed size. </exception>
        /// <remarks></remarks>
        public int Add(object value)
        {
            if (value is T)
            {
                T tValue = (T)value;

                this.Add(tValue);
                return this.IndexOf(tValue);
            }
            return -1;
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.IList"/> contains a specific value.
        /// </summary>
        /// <param name="value">The object to locate in the <see cref="T:System.Collections.IList"/>.</param>
        /// <returns>true if the <see cref="T:System.Object"/> is found in the <see cref="T:System.Collections.IList"/>; otherwise, false.</returns>
        /// <remarks></remarks>
        public bool Contains(object value)
        {
            if (value is T)
            {
                T tValue = (T)value;
                return Contains(tValue);
            }
            return false;
        }

        /// <summary>
        /// Determines the index of a specific item in the <see cref="T:System.Collections.IList"/>.
        /// </summary>
        /// <param name="value">The object to locate in the <see cref="T:System.Collections.IList"/>.</param>
        /// <returns>The index of <paramref name="value"/> if found in the list; otherwise, -1.</returns>
        /// <remarks></remarks>
        public int IndexOf(object value)
        {
            if (value is T)
            {
                T tValue = (T)value;
                return IndexOf(tValue);
            }
            return -1;
        }

        /// <summary>
        /// Inserts an item to the <see cref="T:System.Collections.IList"/> at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index at which <paramref name="value"/> should be inserted.</param>
        /// <param name="value">The object to insert into the <see cref="T:System.Collections.IList"/>.</param>
        /// <exception cref="T:System.ArgumentOutOfRangeException">
        ///   <paramref name="index"/> is not a valid index in the <see cref="T:System.Collections.IList"/>. </exception>
        ///   
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.IList"/> is read-only.-or- The <see cref="T:System.Collections.IList"/> has a fixed size. </exception>
        ///   
        /// <exception cref="T:System.NullReferenceException">
        ///   <paramref name="value"/> is null reference in the <see cref="T:System.Collections.IList"/>.</exception>
        /// <remarks></remarks>
        public void Insert(int index, object value)
        {
            if (value is T)
            {
                T tValue = (T)value;
                Insert(index, tValue);
            }
            else
            {
                throw new NotSupportedException("Value is not the correct type.");
            }
        }

        /// <summary>
        /// Gets a value indicating whether the <see cref="T:System.Collections.IList"/> has a fixed size.
        /// </summary>
        /// <returns>true if the <see cref="T:System.Collections.IList"/> has a fixed size; otherwise, false.</returns>
        /// <remarks></remarks>
        public bool IsFixedSize
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.IList"/>.
        /// </summary>
        /// <param name="value">The object to remove from the <see cref="T:System.Collections.IList"/>.</param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.IList"/> is read-only.-or- The <see cref="T:System.Collections.IList"/> has a fixed size. </exception>
        /// <remarks></remarks>
        public void Remove(object value)
        {
            if (value is T)
            {
                T tValue = (T)value;
                Remove(tValue);
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="System.Object"/> at the specified index.
        /// </summary>
        /// <remarks></remarks>
        object IList.this[int index]
        {
            get
            {
                return this[index];
            }
            set
            {
                if (value is T)
                {
                    T tValue = (T)value;
                    this[index] = tValue;
                }
                else
                {
                    throw new NotSupportedException("Value is not the correct type.");
                }
            }
        }

        /// <summary>
        /// Copies the elements of the <see cref="T:System.Collections.ICollection"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
        /// </summary>
        /// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="T:System.Collections.ICollection"/>. The <see cref="T:System.Array"/> must have zero-based indexing.</param>
        /// <param name="index">The zero-based index in <paramref name="array"/> at which copying begins.</param>
        /// <exception cref="T:System.ArgumentNullException">
        ///   <paramref name="array"/> is null. </exception>
        ///   
        /// <exception cref="T:System.ArgumentOutOfRangeException">
        ///   <paramref name="index"/> is less than zero. </exception>
        ///   
        /// <exception cref="T:System.ArgumentException">
        ///   <paramref name="array"/> is multidimensional.-or- The number of elements in the source <see cref="T:System.Collections.ICollection"/> is greater than the available space from <paramref name="index"/> to the end of the destination <paramref name="array"/>. </exception>
        ///   
        /// <exception cref="T:System.ArgumentException">The type of the source <see cref="T:System.Collections.ICollection"/> cannot be cast automatically to the type of the destination <paramref name="array"/>. </exception>
        /// <remarks></remarks>
        public void CopyTo(Array array, int index)
        {
            T[] tArray = new T[array.Length];
            this.CopyTo(tArray, index);
            tArray.CopyTo(array, index);
        }

        /// <summary>
        /// Gets a value indicating whether access to the <see cref="T:System.Collections.ICollection"/> is synchronized (thread safe).
        /// </summary>
        /// <returns>true if access to the <see cref="T:System.Collections.ICollection"/> is synchronized (thread safe); otherwise, false.</returns>
        /// <remarks></remarks>
        public bool IsSynchronized
        {
            get
            {
                return false;
            }
        }

        private readonly object syncRoot;
        /// <summary>
        /// Gets an object that can be used to synchronize access to the <see cref="T:System.Collections.ICollection"/>.
        /// </summary>
        /// <returns>An object that can be used to synchronize access to the <see cref="T:System.Collections.ICollection"/>.</returns>
        /// <remarks></remarks>
        public object SyncRoot
        {
            get
            {
                return syncRoot;
            }
        }

        #endregion

   }
}
